package pl.edu.pb.wi.pwnography.modules;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import pl.edu.pb.wi.pwnography.model.ParsedData;

public class KNNAllAttributes extends KNearestNeighbor {
    // private static final Logger log = Logger.getLogger(KNNAllAttributes.class
    // .getName());

    public static List<Object> leaveOneOut(ParsedData data,
	    List<String> excludedColumns, String classColumn, int kneighbors,
	    METRIC metric, int rowCount, Map<String, List<Object>> rows)
	    throws Exception {
	if (kneighbors < 1)
	    throw new Exception(
		    "K-NN need two value columns with same size, one class column and k-neighbors >= 1.");
	if (!data.getColumnNames().contains(classColumn))
	    throw new Exception("Class column defined by user doesn't exist.");

	List<String> dataColumns = new ArrayList<String>(
		data.getFloatTypeColumnNames());
	if (excludedColumns != null) {
	    dataColumns.removeAll(excludedColumns);
	}
	if (dataColumns.contains(classColumn)) {
	    dataColumns.remove(classColumn);
	}

	if (dataColumns.size() == 0)
	    throw new Exception("You can't exclude all columns.");

	/*
	 * Nowa kolumna reprezentująca klasy obliczone na podstawie sąsiadów
	 * danych elementów.
	 */
	List<Object> classesBasedOnNeighbors = new ArrayList<Object>();

	/*
	 * Tablica w której są przechowywani sąsiedzi danego punktu. Ma ona
	 * wielkość podaną przez użytkownika.
	 */
	Double[] neighborsDistances = new Double[rowCount - 1 >= kneighbors ? kneighbors
		: rowCount - 1];
	Object[] neighborsClasses = new Object[rowCount - 1 >= kneighbors ? kneighbors
		: rowCount - 1];

	for (int i = 0; i < rowCount; ++i) {
	    // Lecimy po każdym obiekcie.
	    Arrays.fill(neighborsClasses, null);
	    Arrays.fill(neighborsDistances, null);
	    Double maxDistance = null;

	    for (int j = 0; j < rowCount; ++j) {
		// Lecimy po każdym innym obiekcie poza obiektem.
		if (i == j)
		    continue;

		Double distance = Double.valueOf(0.0);
		for (String colName : dataColumns) {
		    // Lecimy po atrybutach.

		    Float first = (Float) rows.get(colName).get(i);
		    Float second = (Float) rows.get(colName).get(j);
		    Float diff = first - second;
		    switch (metric) {
		    case EUCLIDEAN:
			distance = distance + (diff * diff);
			break;
		    // case MAHALANOBIS:
		    // throw new NotImplementedException();
		    case MANHATTAN:
			distance = distance + Math.abs(diff);
			break;
		    default:
			distance += Double.valueOf(0.0);
			break;
		    }
		}

		// Próbujemy wrzucić wartość do tablicy sąsiadów.
		boolean inserted = false;
		for (int x = 0; x < kneighbors; ++x) {
		    if (neighborsDistances[x] == null) {
			neighborsDistances[x] = distance;
			neighborsClasses[x] = rows.get(classColumn).get(j);
			inserted = true;
			break;
		    }
		}

		// Jeżeli wrzucono i nie istnieje najbardziej oddalony sąsiad...
		if (inserted && maxDistance == null) {
		    maxDistance = distance;
		    continue;
		}

		/*
		 * Jeżeli wrzucoco i nowy element jest najdalej oddalonym
		 * sąsiadem...
		 */
		if (inserted && maxDistance != null && maxDistance < distance) {
		    maxDistance = distance;
		    continue;
		}

		/*
		 * Jeżeli nie wrzucono i nowy dystans jest większy niż aktualny
		 * maksymalny...
		 */
		if (!inserted && distance > maxDistance)
		    continue;

		// Jeżeli nie wrzucono i nowy dystans jest mniejszy niż
		// maksymalny...
		if (!inserted && distance < maxDistance) {
		    for (int x = 0; x < kneighbors; ++x) {
			if (neighborsDistances[x] == maxDistance) {
			    neighborsDistances[x] = distance;
			    neighborsClasses[x] = rows.get(classColumn).get(j);
			}
		    }

		    /*
		     * Wyszukujemy nowa wartość maksymalną po zastepieniu starej
		     * nowym elementem.
		     */
		    Double tempMax = null;
		    for (int x = 0; x < kneighbors; ++x) {
			if (tempMax == null) {
			    tempMax = neighborsDistances[x];
			    continue;
			} else if (tempMax < neighborsDistances[x]) {
			    tempMax = neighborsDistances[x];
			}
		    }
		    maxDistance = tempMax;
		}

		/*
		 * Pozostała opcja to: nie wrzucono i dystans jest taki sam jak
		 * maksymalny. Jest to problemem jedynie, gdy oba elementy
		 * różnią się klasami. Na razie zostało pominięte.
		 */
	    }

	    /*
	     * W tym momencie mamy dwie tablice zawierające najbliższych
	     * sąsiadów. To tutaj odbywa się podejmowanie decyzji - głosowanie
	     * itd. itp.
	     */
	    // log.info(String.format("Number of neighbors: %d", kneighbors));
	    // log.info(String.format("Neigbors distances array: %s",
	    // Arrays.toString(neighborsClasses)));
	    // log.info(String.format("Neigbors classes array: %s",
	    // Arrays.toString(neighborsClasses)));

	    // Dodajemy wynik głosowania do listy klas.
	    classesBasedOnNeighbors.add(vote(neighborsDistances,
		    neighborsClasses));
	}

	return classesBasedOnNeighbors;
    }

    public static Object classification(ParsedData data,
	    List<String> excludedColumns, String classColumn,
	    List<Float> objectData, int kneighbors, METRIC metric)
	    throws Exception {
	if (kneighbors < 2)
	    throw new Exception(
		    "K-NN need two value columns with same size, one class column and k-neighbors > 1.");
	if (!data.getColumnNames().contains(classColumn))
	    throw new Exception("Class column defined by user doesn't exist.");

	List<String> dataColumns = new ArrayList<String>(
		data.getFloatTypeColumnNames());
	if (excludedColumns != null) {
	    dataColumns.removeAll(excludedColumns);
	}
	if (dataColumns.contains(classColumn)) {
	    dataColumns.remove(classColumn);
	}

	if (dataColumns.size() != objectData.size())
	    throw new Exception(
		    "Please enter same amount of data, as number of column you selected.");

	for (String col : dataColumns) {
	    if (!data.getColumnNames().contains(col))
		throw new Exception("Selected column doesn't exist.");
	}

	/*
	 * Tablica w której są przechowywani sąsiedzi danego punktu. Ma ona
	 * wielkość podaną przez użytkownika.
	 */
	Double[] neighborsDistances = new Double[data.getRowCount() - 1 >= kneighbors ? kneighbors
		: data.getRowCount() - 1];
	Object[] neighborsClasses = new Object[data.getRowCount() - 1 >= kneighbors ? kneighbors
		: data.getRowCount() - 1];
	Double maxDistance = null;
	Double distance = 0.0;

	maxDistance = null;

	for (int j = 0; j < data.getRowCount(); ++j) {
	    distance = 0.0;
	    for (String colName : dataColumns) {
		// Lecimy po atrybutach.

		switch (metric) {
		case EUCLIDEAN:
		    distance += Math.pow(
			    objectData.get(dataColumns.indexOf(colName))
				    - (Float) data.getCell(colName, j), 2);
		    break;
		// case MAHALANOBIS:
		// throw new NotImplementedException();
		case MANHATTAN:
		    distance += Math.abs(objectData.get(dataColumns
			    .indexOf(colName))
			    - (Float) data.getCell(colName, j));
		    break;
		default:
		    distance += 0.0;
		    break;
		}
	    }

	    // Próbujemy wrzucić wartość do tablicy sąsiadów.
	    boolean inserted = false;
	    for (int x = 0; x < kneighbors; ++x) {
		if (neighborsDistances[x] == null) {
		    neighborsDistances[x] = distance;
		    neighborsClasses[x] = data.getCell(classColumn, j);
		    inserted = true;
		    break;
		}
	    }

	    // Jeżeli wrzucono i nie istnieje najbardziej oddalony sąsiad...
	    if (inserted && maxDistance == null) {
		maxDistance = distance;
		continue;
	    }

	    /*
	     * Jeżeli wrzucoco i nowy element jest najdalej oddalonym
	     * sąsiadem...
	     */
	    if (inserted && maxDistance != null && maxDistance < distance) {
		maxDistance = distance;
		continue;
	    }

	    /*
	     * Jeżeli nie wrzucono i nowy dystans jest większy niż aktualny
	     * maksymalny...
	     */
	    if (!inserted && distance > maxDistance)
		continue;

	    // Jeżeli nie wrzucono i nowy dystans jest mniejszy niż
	    // maksymalny...
	    if (!inserted && distance < maxDistance) {
		for (int x = 0; x < kneighbors; ++x) {
		    if (neighborsDistances[x] == maxDistance) {
			neighborsDistances[x] = distance;
			neighborsClasses[x] = data.getCell(classColumn, j);
		    }
		}

		/*
		 * Wyszukujemy nowa wartość maksymalną po zastepieniu starej
		 * nowym elementem.
		 */
		Double tempMax = null;
		for (int x = 0; x < kneighbors; ++x) {
		    if (tempMax == null) {
			tempMax = neighborsDistances[x];
			continue;
		    } else if (tempMax < neighborsDistances[x]) {
			tempMax = neighborsDistances[x];
		    }
		}
		maxDistance = tempMax;
	    }

	    /*
	     * Pozostała opcja to: nie wrzucono i dystans jest taki sam jak
	     * maksymalny. Jest to problemem jedynie, gdy oba elementy różnią
	     * się klasami. Na razie zostało pominięte.
	     */
	}

	/*
	 * W tym momencie mamy dwie tablice zawierające najbliższych sąsiadów.
	 * To tutaj odbywa się podejmowanie decyzji - głosowanie itd. itp.
	 */
	// log.info(String.format("Number of neighbors: %d", kneighbors));
	// log.info(String.format("Neigbors distances array: %s",
	// Arrays.toString(neighborsClasses)));
	// log.info(String.format("Neigbors classes array: %s",
	// Arrays.toString(neighborsClasses)));

	return vote(neighborsDistances, neighborsClasses);
    }

    public static Map<String, List<Object>> createColumn(String classColumn,
	    List<Object> values, METRIC metric, int kneighbors) {

	/*
	 * Tworzenie nazwy nowej kolumny oraz umieszczenie jej razem z
	 * wartościami w HashMapie.
	 */
	String prefix;
	switch (metric) {
	case EUCLIDEAN:
	    prefix = EUCLIDEAN_DISTANCE_PREFIX;
	    break;
	// case MAHALANOBIS:
	// prefix = MAHALANOBIS_DISTANCE_PREFIX;
	// break;
	case MANHATTAN:
	    prefix = MANHATTAN_DISTANCE_PREFIX;
	    break;
	default:
	    prefix = "NO_PREFIX_";
	    break;
	}
	String prefixedName = new StringBuilder(PREFIX_BASE).append(prefix)
		.append("_").append("ALL_ATTRIBUTES").append("_")
		.append(String.format("%d_NEIGHBORS", kneighbors)).append("_")
		.append(classColumn).append(CLASSES_SUFFIX).toString();
	Map<String, List<Object>> namedKnndColumn = new HashMap<String, List<Object>>();
	namedKnndColumn.put(prefixedName, values);

	return namedKnndColumn;
    }
}
